/**
 * LinkedLST
 * May 13, 2011
 * BarcodeView.java - Barcode scanning activity (for pickup)
 */

package edu.washington.cs.labspecimentransport;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.conn.ConnectTimeoutException;

import zxing.contrib.PlanarYUVLuminanceSource;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.DialogInterface.OnDismissListener;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.hardware.Camera;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Vibrator;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SurfaceView;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.google.zxing.BinaryBitmap;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;

import edu.washington.cs.labspecimentransport.adapters.ColorAdapter;
import edu.washington.cs.labspecimentransport.data.DataQuery;
import edu.washington.cs.labspecimentransport.dataobjects.ItemAttribute;
import edu.washington.cs.labspecimentransport.ops.CommonHandlers;
import edu.washington.cs.labspecimentransport.ops.Conversion;
import edu.washington.cs.labspecimentransport.ops.Keys;
import edu.washington.cs.labspecimentransport.ops.ScannerManager;

/**
 * A BarcodeView displays a scanning view through the camera. Each successful scan is
 * followed by displaying on the screen the results of processing the scan 
 * (not in scan list, already scanned, or scan successful and display scanned barcode).
 * 
 * <p>
 * An intent with the following keys are expected:
 * 	Map<Integer,ArrayList<String>> : KEY_SPECIMENMAP - mapping of ids to barcodes for specimens that need to be picked up
 * 	Map<Integer,ArrayList<String>> : KEY_CONTAINERMAP - mapping of ids to barcodes for containers that need to be picked up	
 */
public class BarcodeView extends Activity {
	
	// variables for zxing operations
    private SurfaceView previewView;// where to draw the camera preview
    private ScannerManager manager; // manages scanning functionality
    private ImageView greyscale; 	// where to draw greyscale scan preview
    private View scanArea; 			// the scanning area
    private TextView txtScanResult; // textfield where scan results will be displayed
    private TextView txtScanCount;	// textfield to display number of items scanned so far
    
    // preferred dimensions for the camera preview frame area
    private int width = 480; 
    private int height = 320; 

    // inner offsets of the scan area from the camera preview frame
    private int dstLeft, dstTop, dstWidth, dstHeight;
    
    private String scannedBarcode;			// the last barcode that was scanned
    private ArrayList<String> scannedItems;	// barcodes that have been scanned so far
    
    // barcode maps for specimen and containers
	private HashMap<Integer, ArrayList<String>> specimenAndBarcodes;
	private HashMap<Integer, ArrayList<String>> containerAndBarcodes;
	
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        // standard initialization, hide the window title, and lock screen in landscape
    	super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setContentView(R.layout.barcode);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        
        getViews();					// get pointers to views
        initializeDataAndViews();	// initialize textfields and start the scanning service
        setupButtons();				// set onClick handlers for all buttons
    }
    
    /**
     * Define the options menu.
     */
    public boolean onCreateOptionsMenu(Menu menu) {
    	menu.add(0, Keys.MENUID_ENTER_BARCODE, 0, R.string.option_enter_barcode);
    	menu.add(0, Keys.MENUID_HOME, 0, R.string.option_home);
    	menu.add(0, Keys.MENUID_HELP, 0, R.string.option_help);
    	return true;
    }

    /**
     * Define functionality for each item in options menu.
     */
    public boolean onOptionsItemSelected(MenuItem item) {   
        switch(item.getItemId()) {
        case Keys.MENUID_ENTER_BARCODE:
        	manager.stopScanning();
        	
        	// inflate a view for the dialog to use
            LayoutInflater factory = LayoutInflater.from(this);
            View view = factory.inflate(R.layout.dialog_textfield, null);
            
            // set properties for the dialog
            AlertDialog.Builder build = new AlertDialog.Builder(BarcodeView.this);
            build.setTitle(getString(R.string.barcode_dialog_barcodeprompt));
            build.setView(view);

            // add a "Submit" button and define its click handler
            build.setPositiveButton(R.string.dialog_submit, 
            	new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) 
                    {
                    	EditText textField = (EditText) 
                    		((AlertDialog)dialog).findViewById(R.id.text_field);
                    	handleScanResult(textField.getText().toString(), 
                    			getString(R.string.barcode_ui_manualSuccess) 
                    			+ " " + textField.getText());
                    }
                });
            // add a "Cancel" button and define its click handler
            build.setNegativeButton(R.string.dialog_cancel, 
            	new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) 
                    {
                    	dialog.dismiss();
                    }
                });
            
            // create the dialog
        	AlertDialog dialog = build.create();
			dialog.setOnDismissListener(new OnDismissListener() {
				public void onDismiss(DialogInterface arg0) {
					// restart scanning when dialog exits
					manager.startScanning();
				}
			});
			
			// finally, show the dialog
            dialog.show();
        	break;
        	
        case Keys.MENUID_HOME:
        	CommonHandlers.returnHome(this);
        	break;
        	
        case Keys.MENUID_HELP:
        	CommonHandlers.startHelp(this);
        	break;
        	
        default:
        	super.onOptionsItemSelected(item);
        	break;
        }
        
        return true;
    }
    
    /**
     * Initializes pointers to views used for scanning
     */
    private void getViews() {
        greyscale = (ImageView) this.findViewById (R.id.greyscalePreview); 
        scanArea = (View) this.findViewById (R.id.scanArea); 
        previewView = (SurfaceView) this.findViewById (R.id.cam_preview); 
        txtScanResult = (TextView) this.findViewById (R.id.barcode_last_scanned);
        txtScanCount = (TextView) this.findViewById(R.id.barcode_number_scanned);
    }
    
    /**
     * Initialize the displays for scanning and start the scanning service
     */
    @SuppressWarnings("unchecked")
	private void initializeDataAndViews() {
    	manager = new ScannerManager (previewView.getHolder(), 
        		width, height, previewCallback); 
        manager.startScanning();
        
        // get list of items to be scanned from intent
        Bundle bundle = this.getIntent().getExtras();

		specimenAndBarcodes = 
			(HashMap<Integer, ArrayList<String>>) bundle.getSerializable(Keys.KEY_SPECIMENMAP);
		containerAndBarcodes = 
			(HashMap<Integer, ArrayList<String>>) bundle.getSerializable(Keys.KEY_CONTAINERMAP);

		// if scannedItems is not null, we are returning from ReviewView
		// and will need to update UI displays
        scannedItems = bundle.getStringArrayList(Keys.KEY_SCANNEDITEMS);
        if (scannedItems == null) {
        	scannedItems = new ArrayList<String>();
        } else {
        	updateCountsDisplay();
        }
    }
    
    /**
     * Sets the onClick handlers for all buttons.
     */
    private void setupButtons() {
        // onClick event handler for "Review" screen
        Button review = (Button) findViewById(R.id.barcode_review);
        review.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {	
            	Intent intent;
            	Bundle newBundle = new Bundle();
            	
            	// if nothing was scanned, display a toast and do nothing
            	if (scannedItems.isEmpty()) {
            		Toast toast = Toast.makeText(getApplicationContext(), 
            				R.string.barcode_error_nothingScanned, Toast.LENGTH_SHORT);
            		toast.show();
            	
            	// otherwise, go to ReviewView and pass it a list of scanned barcodes
            	} else {
                	manager.stopScanning();
                	
                    intent = new Intent(view.getContext(), ReviewView.class);
                    newBundle.putAll(getIntent().getExtras());
                    newBundle.putStringArrayList(Keys.KEY_SCANNEDITEMS, scannedItems);
                    intent.putExtras(newBundle);
                    startActivityForResult(intent, 0);
            	}
            }
        });
    }
    
    /**
     * Start the camera's timer task on resume.
     */
    protected void onResume() {
    	super.onResume();
    	manager.startScanning();
    }
    
	/**
	 * Stop the camera's timer task on pause.
	 */
	protected void onPause() {
		manager.stopScanning();
		super.onPause();
	}
	
	/**
	 * Update list of scanned items on return from ReviewView, to account for
	 * items being removed from the list.	
	 */
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		scannedItems = data.getExtras().getStringArrayList(Keys.KEY_SCANNEDITEMS);
		updateCountsDisplay();
	}
        
    /** 
     * Callback to process scanning area
     */ 
    private Camera.PreviewCallback previewCallback = new Camera.PreviewCallback() { 
        public void onPreviewFrame (byte [] data, Camera arg1) { 
        	// calculate the dimensions of the scanning area
            if (dstLeft == 0) {
                dstLeft = scanArea.getLeft() * width
                		/ getWindowManager().getDefaultDisplay().getWidth();  
                dstTop = scanArea.getTop() * height 
                        / getWindowManager().getDefaultDisplay().getHeight(); 
                dstWidth = (scanArea.getRight() - scanArea.getLeft()) * width 
                        / getWindowManager().getDefaultDisplay().getWidth(); 
                dstHeight = (scanArea.getBottom() - scanArea.getTop()) * height 
                        / getWindowManager().getDefaultDisplay().getHeight(); 
            }
            
            // get the grayscale 
            PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource ( 
                    data, width, height, dstLeft, dstTop, dstWidth, dstHeight);
            Bitmap mBitmap = source.renderCroppedGreyscaleBitmap();
            
            // display grayscale 
            greyscale.setImageBitmap (mBitmap); 
            BinaryBitmap bitmap = new BinaryBitmap (new HybridBinarizer (source)); 
            MultiFormatReader reader = new MultiFormatReader(); 
            try {
                // decode the image using greyscale image
            	Result result = reader.decode(bitmap); 

                ToneGenerator tone=new ToneGenerator(AudioManager.STREAM_ALARM, 50);
                tone.startTone(ToneGenerator.TONE_CDMA_ABBR_ALERT);

                handleScanResult(result.getText(), getString(
                		R.string.barcode_ui_scanSuccess) + " " + result.getText());
            } catch (NotFoundException e) { 
                // do nothing if greyscale did not contain a recognizable barcode
            } 
        } 
    }; 
    
    /**
     * Checks if the scanned barcode is valid and updates the scanning screen
     * @param scannedBarcode the barcode that was scanned
     * @param verbose the message to display to the user on success
     */
    private void handleScanResult(String scannedBarcode, String verbose) {
    	Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
    	v.vibrate(300);
    	
    	this.scannedBarcode = scannedBarcode;
    	
    	if (!Conversion.mapContainsBarcode(specimenAndBarcodes, scannedBarcode) &&
    			!Conversion.mapContainsBarcode(containerAndBarcodes, scannedBarcode)) {
    		// barcode is not on list of barcodes to be scanned
    		Toast toast = Toast.makeText(this, "" + scannedBarcode + 
    				getString(R.string.barcode_msg_scanNotFound), Toast.LENGTH_SHORT);
    		toast.show();
    		
    	} else if (scannedItems.contains(scannedBarcode)) {
    		// barcode has already been scanned
    		Toast toast = Toast.makeText(this, 
    				getString(R.string.barcode_msg_scanAlreadyScanned), Toast.LENGTH_SHORT);
    		toast.show();
    		
        } else {        	
        	// barcode is valid
            final String fBarcode = scannedBarcode;
            final String fVerbose = verbose;           
        	txtScanResult.setText(fVerbose);
        	
        	// display count of vials organized by color if scanned item is a bag
        	if (Conversion.mapContainsBarcode(containerAndBarcodes, scannedBarcode)) {
        		final Dialog dialog = new Dialog(this);
        		
        		// create the dialog to display color count
    			dialog.setContentView(R.layout.dialog_list);
    			
    			// onClick handler for "Confirm"
    			Button confirm = (Button) dialog.findViewById(R.id.dialog_confirm);
    			confirm.setOnClickListener(new View.OnClickListener() {
    				public void onClick(View view) {
    					dialog.dismiss();
    		        	scannedItems.add(fBarcode);
    		            updateCountsDisplay();
    				}
    			});
    			
    			// onClick handler for "Cancel"
    			Button cancel = (Button) dialog.findViewById(R.id.dialog_cancel);
    			cancel.setOnClickListener(new View.OnClickListener() {
    				public void onClick(View view) {	
    					dialog.dismiss();
    				}
    			});

    			// restart
    			dialog.setOnDismissListener(new OnDismissListener() {
    				public void onDismiss(DialogInterface arg0) {
    					// restart 
    					manager.startScanning();
    				}
    			});
			
    			dialog.setTitle(R.string.barcode_dialog_vialHeader);
    			
        		// list of items that need to be scanned for a location
        		// create the dialog to display color count
        		new DisplayColors(dialog).execute();
        	} else {
            	scannedItems.add(scannedBarcode);
                updateCountsDisplay();
            }
        }
    }
    
    /**
     * Updates the descriptive text for number of items scanned
     */
    private void updateCountsDisplay() {
    	int countOfScanned = scannedItems.size();
    	String strCount;
    	strCount = "" + countOfScanned + "/" + 
    		( containerAndBarcodes.size() + specimenAndBarcodes.size() ) + 
    		" " + getString(R.string.barcode_msg_scanCount);
    	
    	txtScanCount.setText(strCount);
    }
        
    /**
     * Service to display dialog listing the count of vials in a container, 
     * list is organized by color.
     */
	private class DisplayColors extends AsyncTask<Void, Void, Void> {
		private ProgressDialog progressDialog;
		private boolean success;
		private boolean timeout;
		private Dialog dialog;
		
		public DisplayColors(Dialog dialog) {
			this.dialog = dialog;
		}

		@Override
		protected void onPreExecute() {
			super.onPreExecute();
			success = true;
			timeout = false;
    		// stop scanning until dialog is dismissed
    		manager.stopScanning();

    		progressDialog = ProgressDialog.show(BarcodeView.this, "",
    				getString(R.string.barcode_load_fetchColors), true, true,
					new DialogInterface.OnCancelListener(){
				public void onCancel(DialogInterface dialog) {
					DisplayColors.this.cancel(true);
					finish();
				}
			});
		}		
		
		@Override
		protected Void doInBackground(Void... params) {
    		try {
    			// query the color counts for the scanned container
    			DataQuery query = DataQuery.getInstance();
    			Map<ItemAttribute,Integer> colorMap = query.getCountOfColorsForBag(scannedBarcode);

    			// set the adapter for displaying list of color counts
    			ColorAdapter adapter = new ColorAdapter(BarcodeView.this, colorMap);

    			ListView listView = (ListView) dialog.findViewById(R.id.dialog_list);
    			listView.setAdapter(adapter);
    		} catch (NullPointerException e) {
    			success = false;
    		} catch (ConnectTimeoutException e) {
    			timeout = true;
    		}
			return null;
		}

		@Override
		protected void onPostExecute(Void result) {
			setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
			progressDialog.dismiss();
			if (success) {
				dialog.show();
			} else if (timeout) {
				Toast toast = Toast.makeText(
						BarcodeView.this, R.string.error_timeout,
						Toast.LENGTH_SHORT);
				toast.show();
			} else {
				Toast toast = Toast.makeText(
						BarcodeView.this, R.string.error_default,
						Toast.LENGTH_SHORT);
				toast.show();
			}
		}
	}
}
